﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Runtime.InteropServices;

namespace Upreader.Usenet.Nntp
{
    public class NntpStreamReader : TextReader
    {
        #region Configuration
        
        private const int DefaultBufferSize = 1024;
        private const int DefaultFileBufferSize = 4096;
        private const int MinimumBufferSize = 512;
        private const int MaximumLineBuilderCacheSize = 1024 * 1024 * 1024; // 1 mb

        #endregion // Configuration

        #region Fields
        
        private readonly byte[] _inputBuffer;
        private readonly char[] _decodedBuffer;

        private readonly int _bufferSize;
        private readonly Encoding _encoding;
        private readonly Stream _baseStream;

        // Decoded bytes in m_decodedBuffer.
        private int _decodedCount;
        // Current position in the m_decodedBuffer
        private int _currentDecodePosition;

        private Decoder _decoder;

        private bool _mayBlock;
        private bool _foundCR;
        private StringBuilder _lineBuilder;

        #endregion // Fields

        #region Constructors

        public NntpStreamReader(Stream stream, Encoding encoding)
            : this(stream, encoding, DefaultBufferSize) 
        { }

        public NntpStreamReader(Stream stream, Encoding encoding, int bufferSize)
        {
            if (stream == null)
                throw new ArgumentNullException("stream");
   
            if (encoding == null)
                throw new ArgumentNullException("encoding");
            
            if (!stream.CanRead)
                throw new ArgumentException(Resource.NntpStreamReader_Unreadable);

            /*if (bufferSize <= MinimumBufferSize)
                throw new ArgumentException(Resource.NntpStreamReader_InvalidBufferSize, "bufferSize");*/

            _baseStream = stream;
            _inputBuffer = new byte[bufferSize];
            _bufferSize = bufferSize;
            _encoding = encoding;
            _decoder = encoding.GetDecoder();

            _decodedBuffer = new char[encoding.GetMaxCharCount(bufferSize)];
            _decodedCount = 0;
            _currentDecodePosition = 0;
        }

        #endregion // Constructor

        #region Properties
        
        public virtual Stream BaseStream
        {
            get { return _baseStream; }
        }

        public virtual Encoding CurrentEncoding
        {
            get { return _encoding; }
        }

        public bool EndOfStream
        {
            get { return Peek() < 0; }
        }

        #endregion // Properties

        #region Methods

        public override void Close()
        {
            Dispose(true);
        }

        /// <summary>
        /// Discards any data in the cache and reset
        /// </summary>
        public void Reset()
        {
            _decoder = _encoding.GetDecoder();
            _currentDecodePosition = 0;
            _decodedCount = 0;
            _mayBlock = false;
        }

        public override int Peek()
        {
            CheckObjectState();

            if (_currentDecodePosition >= _decodedCount && (_mayBlock || ReadBuffer() == 0))
            {
                return -1;
            }

            return _decodedBuffer[_currentDecodePosition];
        }

        public override int Read()
        {
            CheckObjectState();

            if (_currentDecodePosition >= _decodedCount && ReadBuffer() == 0)
            {
                return -1;
            }

            return _decodedBuffer[_currentDecodePosition++];
        }

        public override int Read([In, Out] char[] destinationBuffer, int index, int count)
        {
            CheckObjectState();

            if (destinationBuffer == null)
            {
                throw new ArgumentNullException("destinationBuffer");
            }
            if (index < 0)
            {
                throw new ArgumentOutOfRangeException("readBufferIndex");
            }
            if (count < 0)
            {
                throw new ArgumentOutOfRangeException("count");
            }

            if (index > (destinationBuffer.Length - count))
            {
                throw new ArgumentOutOfRangeException("readBufferIndex");
            }

            int charsRead = 0;
            while (count > 0)
            {
                if (_currentDecodePosition >= _decodedCount && ReadBuffer() == 0)
                {
                    return charsRead > 0 ? charsRead : 0;
                }

                int cch = Math.Min(_decodedCount - _currentDecodePosition, count);
                Array.Copy(_decodedBuffer, _currentDecodePosition, destinationBuffer, index, cch);
                _currentDecodePosition += cch;
                index += cch;
                count -= cch;
                charsRead += cch;
            }
            return charsRead;
        }

        public override string ReadLine()
        {
            CheckObjectState();

            if (_currentDecodePosition >= _decodedCount && ReadBuffer() == 0)
            {
                return null;
            }

            int begin = _currentDecodePosition;
            int end = FindNextEOL();
            if (end < _decodedCount && end >= begin)
            {
                return new string(_decodedBuffer, begin, end - begin);
            }

            if (_lineBuilder == null)
            {
                _lineBuilder = new StringBuilder();
            }
            else
            {
                _lineBuilder.Length = 0;
            }

            while (true)
            {
                if (_foundCR) // don't include the trailing CR if present
                {
                    _decodedCount--;
                }

                _lineBuilder.Append(_decodedBuffer, begin, _decodedCount - begin);
                if (ReadBuffer() == 0)
                {
                    if (_lineBuilder.Capacity > MaximumLineBuilderCacheSize)
                    {
                        StringBuilder sb = _lineBuilder;
                        _lineBuilder = null;
                        return sb.ToString(0, sb.Length);
                    }
                    return _lineBuilder.ToString(0, _lineBuilder.Length);
                }

                begin = _currentDecodePosition;
                end = FindNextEOL();
                if (end < _decodedCount && end >= begin)
                {
                    _lineBuilder.Append(_decodedBuffer, begin, end - begin);
                    if (_lineBuilder.Capacity > MaximumLineBuilderCacheSize)
                    {
                        StringBuilder sb = _lineBuilder;
                        _lineBuilder = null;
                        return sb.ToString(0, sb.Length);
                    }
                    return _lineBuilder.ToString(0, _lineBuilder.Length);
                }
            }
        }

        public override string ReadToEnd()
        {
            CheckObjectState();

            StringBuilder text = new StringBuilder();

            bool isMoreToRead = true;

            while (isMoreToRead)
            {
                string line = ReadLine();
                if (line == ".") // means the end of the line
                {
                    isMoreToRead = false;
                }
                else
                {
                    text.AppendLine(line);
                }
            }

            return text.ToString();
        }

        #endregion // Methods

        #region Dispose

        protected override void Dispose(bool disposing)
        {
            try
            {
                if (disposing && _baseStream != null)
                {
                    _baseStream.Close();
                }
            }
            finally
            {
                base.Dispose(disposing);
            }
        }


        #endregion // Dispose

        #region Helpers

        private int ReadBuffer()
        {
            _currentDecodePosition = 0;
            int encodedBytes = 0;

            _decodedCount = 0;
            int parse_start = 0;
            do
            {
                encodedBytes = _baseStream.Read(_inputBuffer, 0, _bufferSize);

                if (encodedBytes <= 0)
                {
                    return 0;
                }

                _mayBlock = (encodedBytes < _bufferSize);

                _decodedCount += _decoder.GetChars(_inputBuffer, parse_start, encodedBytes, _decodedBuffer, 0);
                parse_start = 0;
            } while (_decodedCount == 0);

            return _decodedCount;
        }

        private int FindNextEOL()
        {
            char c = '\0';
            for (; _currentDecodePosition < _decodedCount; _currentDecodePosition++)
            {
                c = _decodedBuffer[_currentDecodePosition];
                if (c == '\n' && _foundCR)
                {
                    _currentDecodePosition++;
                    int res = (_currentDecodePosition - 2);
                    if (res < 0)
                    {
                        res = 0; // if a new array starts with a \n and there was a \r at the end of the previous one, we get here.
                    }
                    _foundCR = false;
                    return res;
                }

                _foundCR = (c == '\r');
            }

            return -1;
        }

        private void CheckObjectState()
        {
            if (_baseStream == null)
            {
                throw new InvalidOperationException(Resource.ObjectDisposed);
            }
        }

        #endregion // Helpers
    }
}
